# ---------------------------------------------------------------------------
# Author: /\/\o\/\/ with contributions from Sung Kim
# Name:   TabExpansion
# Desc:   This is the default function that gets called for tab expansion.
# Date:   Dec 2, 2006
# Site:   http://www.codeplex.com/PowerShellCX
# Usage:  In your profile place the following command:
#
#         . "$Env:PscxHome\Profile\TabExpansion.ps1"
#
# History:
#  - Cached tab completion on types (janel / mow).
#  - Methods and properties of types
#  - shows get_ methods
#  - MultiLevel variable Tab Completion
#  - Bracet Removal
# Edited by Sung Kim: Added Progressbar and Scoped variable name expansion
#  - MultiLevel variable Tab Completion for Scoped variables
# Roman Kuzmin: Fixed static-member iteration problem
# Keith Hill: Trying to eliminate global scope pollution
# ---------------------------------------------------------------------------
function TabExpansion {
    param($line, $lastWord)

    begin {
        $global:__pscxTabExpansionMethods = `
            [System.Management.Automation.PSMemberTypes]`
                'Methods,ParameterizedProperty'

        function CreateAssemblyCacheTable {
            if (!($global:__pscxTabExpansionAssembyCache)) {
                $global:__pscxTabExpansionAssembyCache = `
                    New-Object System.Data.Datatable
                [void]($global:__pscxTabExpansionAssembyCache.Columns.Add(`
                    'name',[string]))
                [void]($global:__pscxTabExpansionAssembyCache.Columns.Add(`
                    'DC',[int]))
                [void]($global:__pscxTabExpansionAssembyCache.Columns.Add(`
                    'NS',[string]))
                $assemblies = [appdomain]::CurrentDomain.GetAssemblies()
                [void] ($assemblies | foreach {$i = 0} {
                    $i++;
                    [int]$private:assemblyProgress = `
                        ($i * 100) / $assemblies.Length
                    Write-Progress Caching `
                        "$private:assemblyProgress for '$($_.ManifestModule.ScopeName)'" `
                        -perc $private:assemblyProgress

                    $types = $_.GetTypes()
                    $types | foreach {$j = 0} {
                        $j++;
                        if (($j % 500) -eq 0) {
                            [int]$typeProgress = `
                                ($j * 100) / $types.Length
                            Write-Progress "Adding types '$($_.FullName)'" `
                                "$typeProgress" -perc $typeProgress -id 1
                        }

                        $dc = $_.fullName.split(".").count - 1
                        $ns = $_.namespace
                        $global:__pscxTabExpansionAssembyCache.rows.add(`
                            "$_", $dc, $ns)
                    }

                    Write-Progress "Adding types" "100" -perc 100 -id 1
                })
            }
        }

        function CreateWmiCacheTable {
            if (!($global:__pscxTabExpansionWmiCache)) {
                $global:__pscxTabExpansionWmiCache = new data.datatable
                [void]($global:__pscxTabExpansionWmiCache.Columns.add(`
                    'Name',[string]))
                [void]($global:__pscxTabExpansionWmiCache.Columns.add(`
                    'Description',[string]))

                $WmiClass = [WmiClass]''

                # Set Enumeration Options

                $opt = New-Object System.Management.EnumerationOptions
                $opt.EnumerateDeep = $True
                $opt.UseAmendedQualifiers = $true

                $i = 0
                write-progress "Adding WMI Classes" "$i"

                $WmiClass.psBase.GetSubclasses($opt) | `
                foreach {
                    $i++
                    # Update progressbar for every 10 percent
                    if (($i % 10) -eq 0) {
                        write-progress "Adding WMI Classes" "$i"
                    }
                    [void]$global:__pscxTabExpansionWmiCache.rows.add($_.name,`
                        ($_.psbase.Qualifiers | `
                        Where-Object {$_.Name -eq 'description'} | `
                        foreach {$_.Value}))
                }
                write-progress "Adding WMI Classes" "$i" -Completed
            }
        }

        function MultiLevelPropertyExpansionHandler {
            param($parent, $child)

            $private:explodedChild = $child.split('.')
            $private:level = $explodedChild.Count
            $private:value = $parent

            if ($level -gt 1) {
                foreach ($childItem in $explodedChild[0..($level - 2)]) {
                    $private:value += '.' + $childItem
                }
            }
            $private:pattern = $private:explodedChild[($level -1)] + '*'

            # /\/\o\/\/ removed : -and $private:n -notmatch '^[ge]et_'
            # to get get_ methods on WMI and AD objects
            $result = @()
            $private:inputObject = '$' + $private:value
            Invoke-Expression "Get-Member -InputObject $inputObject" |
            Sort-Object name | Where-Object {$_.name -like $pattern } |
            foreach {
                $result += @{
                    MemberType = $_.MemberType;
                    Name = $_.Name;
                    Value = $private:value;
                }
            }

            return $result
        }

        # Handle property and method expansion on simple "(...)" 
        # blocks(By /\/\o\/\/)
        # where "$expression" is expression inside parenthesis
        #     and $prefix is a prefix for a property/method name
        # e.g.) PS> (gps).C[Tab] --> (gps).Count
        #     then "$expression" refers to "gps"
        #     and "$prefix" refers to "C" in "Count"
        function SimpleExpressionHandler {
            param([string]$expression, [string]$prefix)

            Invoke-Expression "Get-Member -inputobject $expression" |`
            Where-Object {$_.name -like $prefix} |`
            foreach {
                if ($_.MemberType -band $global:__pscxTabExpansionMethods) {
                    # Return a method...
                    $lastword.Substring(0, $lastword.LastIndexOf('.')) + '.' `
                        + $_.name + '('
                } else {
                    # Return a property...
                    $lastword.Substring(0, $lastword.LastIndexOf('.')) + '.' `
                        + $_.name
                }
            }
        }

        function WmiClassHandler {
            param([string]$prefix)

            $global:__pscxTabExpansionWmiCache.select(`
                "name like '$($prefix)%'") |
                foreach {
                    $_.name
                }
        }

        function StaticExpressionHandler {
            param([string]$type, [string]$name)

            Invoke-Expression "$type | gm -static" |
            Where-Object {
            $n = $_.Name
            $n -like "$name*" -and `
            $n -notmatch '^([gs]et|op)_'} |
            foreach {
                if ($_.MemberType -band $global:__pscxTabExpansionMethods) {
                    "$($type)::$($_.name)" + '('
                }
                else {
                    "$($type)::$($_.name)"
                }
            }
            break;
        }

        function TypeNameHandler {
            param([string]$prefix)

            # actual tab completion
            $dots = $prefix.split(".").count - 1
            switch ($dots) {
                0 {"[System","[Microsoft"}
                Default {
                    $res = @()
                    $res += $global:__pscxTabExpansionAssembyCache.select(`
                        "ns like '$($prefix)%' and dc = $($dots + 1)") |`
                        Select-Object -Unique ns | foreach {"[$($_.ns)"};
                    $res += $global:__pscxTabExpansionAssembyCache.select(`
                        "name like '$($prefix)%' and dc = $dots") |`
                        foreach {"[$($_.name)]"}
                    $res
                }
            }
        }
    }

      end {
        switch -regex ($line)  {
            # Handle property and method expansion
            # on simple () blocks( added by /\/\o\/\/)...
            # E.g.) PS> (ls).[tab] ---> (ls).Count
            '^\((.+)\)\.(.*)' {
                # Run inside another scope not to pollute global scope
                # with "$expression" and "$prefix" variables
                & {
                    $expression = "($($matches[1]))"
                    $prefix = "$($matches[2])*"
                    SimpleExpressionHandler $expression $prefix
                }
                break;
            }
        }

        switch -regex ($lastWord) {
            # Expand WMI class names
            '(Win32_.*|Cim_.*|MSFT_.*)' {
                # Run inside another scope not to pollute global scope
                & {
                    $prefix = $matches[1]
                    CreateWmiCacheTable
                    WmiClassHandler $prefix
                }
                break;
            }
            #endregion

            # Handle methods of Types (/\/\o\/\/).
            # E.g.) [DateTime]::F[tab]
            '(\[.*\])::(\w*)' {
                # Run inside another scope not to pollute global scope
                & {
                    $type = $matches[1]
                    $name = $matches[2]
                    StaticExpressionHandler $type $name
                }
                break;
            }
            #endregion

            # Remove Brackets from typename (/\/\o\/\/)
            # Note: Type name expansion relies on type name being inside "[]"
            # brackets, when creating a new object, one would have to remove 
            # brackets around type name.
            #  This feature would remove brackets when "=" is typed afterwards
            # E.g.) Suppose that you are creating a "Ping" object
            #  (Tab press is represented with "{tab}")
            #  $ping = New-Object [System.N{tab}.Net{tab}.P{tab}]
            #  Now you are left with
            #  $ping = New-Object [System.Net.NetworkInformation.Ping]
            #  To remove brackets, now you can simply add "=" after the bracket
            #  and press tab
            #  $ping = New-Object [System.Net.NetworkInformation.Ping]={tab}
            #  Now brackets are stripped.
            #  $ping = New-Object System.Net.NetworkInformation.Ping
            '(\[.*\])=(\w*)' {
                "$($matches[1].replace('[','').replace(']',''))"
                break;
            }
            #endregion

            # Cache and Handle namespace and TypeNames (/\/\o\/\/)
            '\[(.*)' {
                # Run inside another scope not to pollute global scope
                & {
                    $prefix = $matches[1]
                    # The following method will create a type name cache
                    # only when a cache does not already exist
                    CreateAssemblyCacheTable

                    # Retrieve type name matching the specified prefix
                    TypeNameHandler $prefix
                }
                break;
            }
            #endregion

            # Handle property and method expansion (MultiLevel added /\/\o\/\/)
            '\$(\w+)\.(.*)' {
                # Run inside another scope not to pollute global scope
                & {
                    # parent variable name e.g.) $host.ui.-> parent is "host"
                    $parent = $matches[1];
                    # child variable name 
                    # e.g.) $host.ui.rawui-> child is "ui.rawui"
                    $child = $matches[2]

                    MultiLevelPropertyExpansionHandler $parent $child |
                    foreach {
                        $output = "$" + $_["Value"] + '.' + $_["Name"]
                        if ($_["MemberType"] -band `
                            $global:__pscxTabExpansionMethods) {
                            $output += "("
                        }
                        return $output
                    }
                }
                break;
            }
            #endregion

            # Handle expansions for both "Scope Variable Name" and 
            # "Type Variable Names"
            '(.*^\$)(\w+):(\w+)(\.(.*))*$' {
                # Run inside another scope not to pollute global scope
                & {
                    # "function", "variable", "alias", "env:", etc...
                    $type = $matches[2];
                    # E.g.) '$' + 'function'
                    $prefix = $matches[1] + $type;
                    # e.g. in '$function:C', $typeName will be 'C'
                    $typeName = $matches[3];
                    # e.g. in '$global:host.ui.rawui', child will be 'ui.rawui'
                    $child = $matches[5];

                    # Scope Variable Name Expansion for 
                    # $global:, $private:, $script:, $local:
                    $scopeNames = @("global", "local", "script", "private")
                    if ($scopeNames -contains $type) {
                        if ($child) {
                            # Handle *multi-level* property and method expansion
                            MultiLevelPropertyExpansionHandler $typeName $child|
                            foreach {
                                $output = $prefix + ":" + $_["Value"] + '.' + `
                                    $_["Name"]
                                # Append "(" for method
                                if ($_["MemberType"] -band `
                                    $global:__pscxTabExpansionMethods) {
                                    $output += '('
                                }
                                return $output
                            }
                        } else {  # Expand scope variable name
                            foreach ($scopeVariable in
                                (Get-Variable "$($typeName)*" -Scope $type |
                                Sort-Object name)) {
                                $prefix + ":" + $scopeVariable.Name
                            }
                        }
                    } else {
                        # Type name expansion for 
                        # $function:, $variable, $env:, etc
                        foreach ($t in
                            (Get-ChildItem ($type + ":" + $typeName + '*') |
                            Sort-Object name)) {
                            $prefix + ":" + $t.Name
                        }
                    }
                }

                break;
            }
            #endregion

            # Default functions below this line

            # Default: Handle property and method expansion...
            '\$(\w+)\.(\w*)' {
                & {
                    $variableName = $matches[1]
                    $val = Get-Variable -value $variableName
                    $pat = $matches[2] + '*'
                    Get-Member -inputobject $val |
                    Where-Object { $n = $_.name; `
                        $n -like $pat -and $n -notmatch '^([gs]et|op)_' } |
                        foreach {
                            if ($_.MemberType -band `
                                $global:__pscxTabExpansionMethods) {
                                # Return a method...
                                '$' + $variableName + '.' + $_.name + '('
                            } else {
                                # Return a property...
                                '$' + $variableName + '.' + $_.name
                            }
                        }
                }
                break;
            }

            # Default: Handle variable name expansion...
            '(.*^\$)(\w+)$' {
                & {
                    $prefix = $matches[1]
                    $varName = $matches[2]
                    $variables = Get-Childitem ('variable:' + $varName + '*')
                    foreach ($v in $variables) {
                        $prefix + $v.name
                    }
                }
                break;
            }

            # Default: Do completion on parameters (original) ...
            '^-([\w0-9]*)' {
                & {
                    $pat = $matches[1] + '*'

                    # extract the command name from the string
                    # first split the string into statements & pipeline elements
                    # This doesnt handle strings however.
                    $cmdlet = [regex]::Split($line, '[|;]')[-1]

                    #  Extract the trailing unclosed block
                    if ($cmdlet -match '\{([^\{\}]*)$') {
                        $cmdlet = $matches[1]
                    }

                    # Extract the longest unclosed parenthetical expression
                    if ($cmdlet -match '\(([^()]*)$') {
                        $cmdlet = $matches[1]
                    }

                    # take the first space separated token of the remaining 
                    # string as the command to look up. 
                    # Trim any leading or trailing spaces
                    # so you dont get leading empty elements.
                    $cmdlet = $cmdlet.Trim().Split()[0]

                    # now get the info object for it...
                    $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet)[0]

                    # loop resolving aliases...
                    while ($cmdlet.CommandType -eq 'alias') {
                        $cmdlet = @(Get-Command -type 'cmdlet,alias' `
                            $cmdlet.Definition)[0]
                    }

                    # expand the parameter sets and emit the matching elements
                    foreach ($private:n in ($cmdlet.ParameterSets |
                        Select-Object -expand parameters)) {
                        $private:n = $private:n.name
                        if ($private:n -like $pat) {
                            '-' + $private:n
                        }
                    }
                }
                break;
            }

            # Handle "about_*" help manuals
            '\s*(about_[\w|\d]*)' {
                & {
                    # extract the command name from the string
                    # (taken from the default argument expansion)
                    $cmdlet = [RegEx]::Split($line, '[|;]')[-1]

                    # Check if cmdlet starts with either "get-help" or "help"
                    $colMatch = [RegEx]::Matches($cmdlet, `
                        '\s*(?i:((get-)?help)|man)\s*((?i:about_)[\w|\d]*)')

                    # there was no "get-help about_" pattern.
                    if (($colMatch.Count -le 0) -or (!$colMatch[0].Success)) {
                        break;
                    }

                    # I am not sure why $x[0].Groups[-1].Value wouldn't work
                    $numGroups = $colMatch[0].Groups.Count
                    $szAbout = `
                        $colMatch[0].Groups[$numGroups - 1].Value.Trim()
                    $helpFileNames = Get-Help $szAbout -ea SilentlyContinue | `
                        Sort-Object Name | Select-Object Name

                    # if there were help manuals matching user's 
                    # "about_*" string,
                    if ($helpFileNames -ne $null) {
                        $helpFileNames | `
                        foreach {
                            # extract manual name(NOTE:same RegEx pattern)
                            $m = [RegEx]::Matches($_.Name,'\s*(about_[\w|\d]*)')
                            $m[0].Value
                        }
                    }
                }
                break;
            }

            # Handle 'Alias' expansion
            '(?<token>(\S)*)!' {
                & {
                    # alias candidate
                    $alias = $matches.token;

                    # If the matched string is an alias, expand it.
                    if (Test-Path -Path Alias:$alias) {
                        return (Get-Item Alias:$alias).Definition
                    }
                }
                break;
            }

            #endregion
        }  # switch
      }  # process
}